import os.path
import re
from collections import defaultdict

from .common_func.print_logger import PrintLogger as Logger
import_error_warned = False
try:
    from lxml import etree
except ModuleNotFoundError:
    if not import_error_warned:
        Logger.warning("UXml is not valid, because require module:[lxml] is not installed. "
                       "If you want use UXml, please install [lxml] first")
        import_error_warned = True

class XmlPos:
    TAG_START = 1
    TAG_END = 2
    INIT = 3
    TEXT = 4
    QUOTA_TEXT = 5

class UXml:
    @classmethod
    def _extract_leaf_api_format(cls, element, father_path="") -> list:
        result = list()
        if element is None:
            return result
        if "}" in element.tag:
            idx = element.tag.rfind('}')
            new_tag = element.tag[idx + 1:]
        else:
            new_tag = element.tag

        cur_path = cls._path_join(father_path, new_tag, formatter="linux")
        if not len(element):
            item = {"path": cur_path, "name": os.path.basename(cur_path), "value": element.text}
            result.append(item)
        else:
            for child in element:
                result = result + cls._extract_leaf_api_format(child, cur_path)
        return result

    @classmethod
    def _extract_leaf(cls, element, father_path="") -> dict:
        if not element:
            return dict()
        result = defaultdict(set)
        if "}" in element.tag:
            idx = element.tag.rfind('}')
            new_tag = element.tag[idx + 1:]
        else:
            new_tag = element.tag

        cur_path = cls._path_join(father_path, new_tag, formatter="linux")
        if not len(element):
            result[cur_path].add(element.text)
        else:
            for child in element:
                result.update(cls._extract_leaf(child, cur_path))
        return result

    @classmethod
    def extract_leaf(cls, element, res_format: str = "default"):
        if res_format == "api":
            return cls._extract_leaf_api_format(element)
        else:
            return cls._extract_leaf(element)

    @classmethod
    def remove_rnt(cls, xml_str: str):
        return xml_str.replace('\n', '').replace('\r', '').replace('\t', '')

    @classmethod
    def remove_property_and_rnt(cls, xml_str: str):
        # 先处理注释
        _pos = xml_str.find("<!--")
        while _pos != -1:
            end_pos = xml_str.find("-->")
            if end_pos == -1:
                xml_str = xml_str[:_pos]
            else:
                xml_str = xml_str[:_pos] + xml_str[end_pos + 3:]
            _pos = xml_str.find("<!--")
        res = ""
        start_pos = 0
        pos = xml_str.find("<", 0)
        while pos != -1:
            res += xml_str[start_pos: pos]
            r_pos = xml_str.find(">", pos)
            if r_pos == -1:
                break
            tag = xml_str[pos:r_pos]
            if not tag.startswith("<?xml") and not tag.startswith("<?html"):
                tag = tag.split()[0]
            new_tag = tag + ">"
            res += new_tag
            start_pos = r_pos + 1
            pos = xml_str.find("<", r_pos)
        res = re.sub(r'<\?xml.*\?>', '', res, count=1).strip()
        return res.replace('\n', '').replace('\r', '').replace('\t', '')

    @classmethod
    def _is_leaf(cls, xml_str: str):
        if "<" in xml_str and ">" in xml_str and "</" in xml_str:
            return False
        else:
            return True

    @staticmethod
    def _get_sub_str(origin_str: str, start_flag: str = "", end_flag: str = "", start_pos: int = 0):
        """
        在原始字符串中通过开始和结束标识字符串找到目标子串
        :param origin_str:
        :param start_flag:
        :param end_flag:
        :param start_pos:
        :return:
        """
        if start_flag and start_flag not in origin_str:
            return ""
        if end_flag and end_flag not in origin_str:
            return ""

        if start_flag:
            sub_str_start_pos = origin_str.find(start_flag, start_pos) + len(start_flag)
        else:
            sub_str_start_pos = 0
        if end_flag:
            sub_str_end_pos = origin_str.find(end_flag, sub_str_start_pos)
        else:
            sub_str_end_pos = len(origin_str)

        return origin_str[sub_str_start_pos:sub_str_end_pos]

    @classmethod
    def get_first_tag(cls, xml_str: str):
        return cls._get_sub_str(xml_str, "<", ">")

    @classmethod
    def _fix_xml_str(cls, xml_str: str):
        """
        修复破坏的xml字符串
        :param xml_str:
        :return:
        """
        xml_str = xml_str.strip()
        start_pos = xml_str.find('<')
        last_close_pos = xml_str.rfind('</')
        if start_pos == -1 or last_close_pos == -1:
            return ""
        end_pos = xml_str.find('>', last_close_pos)
        if end_pos == -1:
            end_pos = last_close_pos - 1

        xml_str = xml_str[start_pos:end_pos + 1]

        unmatched_tag_list = list()
        fix_str = ""
        remain_str = xml_str
        while remain_str:
            tag = cls.get_first_tag(remain_str)
            if not tag or not remain_str.startswith(f"<{tag}>"):
                return fix_str
            if tag.startswith("?"):
                fix_str = fix_str + f"<{tag}>"
                remain_pos = len(f"<{tag}>")
            else:
                end_pos = remain_str.find(f"</{tag}>")
                if end_pos == -1:
                    unmatched_tag_list.append(tag)
                    fix_str = fix_str + f"<{tag}>"
                    remain_pos = len(f"<{tag}>")
                else:
                    fix_str = fix_str + remain_str[:end_pos] + f"</{tag}>"
                    remain_pos = end_pos + len(f"</{tag}>")
            remain_str = remain_str[remain_pos:]

        while unmatched_tag_list:
            tag = unmatched_tag_list.pop()
            fix_str = fix_str + f"</{tag}>"

        return fix_str

    @classmethod
    def auto_fix_loads(cls, xml_str, cur_try: int = 0, max_try: int = 2):
        xml_str = cls.remove_property_and_rnt(xml_str)
        return cls._auto_fix_loads(xml_str, cur_try, max_try)

    @classmethod
    def _auto_fix_loads(cls, xml_str, cur_try: int = 0, max_try: int = 2):
        xml_str = xml_str.strip()

        try:
            return cls.loads(xml_str)
        except Exception as e:
            if cur_try >= max_try:
                Logger.error(f"Auto fix of xml failed after try {cur_try} times, "
                             f"now fix xml string is :\n{xml_str}\nError msg: {e}")
                return None
            fix_str = cls._fix_xml_str(xml_str)
            if not fix_str:
                return None
            return cls._auto_fix_loads(fix_str, cur_try + 1, max_try)

    @classmethod
    def split_xml(cls, xml_str: str):
        res = dict()
        remain_str = xml_str.strip()
        while remain_str:
            first_tag = cls.get_first_tag(remain_str)
            if not first_tag:
                return res
            end_tag_len = len(first_tag) + 3  # 加上两边的</ 和>
            sub_start = f"<{first_tag}>"
            sub_end = f"</{first_tag}>"
            content = cls._get_sub_str(remain_str, sub_start, sub_end)
            res[first_tag] = content
            sub_end_pos = remain_str.find(sub_end)
            remain_str = remain_str[sub_end_pos + end_tag_len:]
        return res


    @classmethod
    def parse(cls, xml_data):
        return etree.XML(xml_data)

    @classmethod
    def loads(cls, xml_str: str):
        return cls.parse(xml_str.encode())

    @staticmethod
    def _path_join(*_path, formatter: str = ""):
        """
        替代os.path.join
        :param _path:
        :param formatter 路径格式，默认为空，根据操作系统自适应，如果指定linux或windows， 则按照指定的操作系统路径格式执行
        :return:
        """
        if formatter.lower() == "windows":
            sep = "\\"
        elif formatter.lower() == "linux":
            sep = "/"
        else:
            return os.path.join(*_path)

        return_path = ""
        for s_path in _path:
            if s_path:
                return_path = return_path + s_path + sep
        return return_path[:-1]

"""
# a = "<title>四种Python爬虫常用的定位元素方法对比，你偏爱哪一款？</title><script type=text/javascript>window.startTime = Date.now();</script>"
# b = UXml.remove_property_and_rnt(a)
# c = UXml.get_first_tag(b)
# print(c)
# print(UXml.my_extract(b))
# print(UXml.my_new_extract(b))
# import time
# import xmltodict
# from test_data import TEST_DATA
#
# test_data = TEST_DATA
# xml_data = xmltodict.unparse({"data": test_data}, pretty=True)
# _s = time.time()
# _root = UXml.remove_property_and_rnt(xml_data)
# # ret_dict = UXml.my_new_extract(_root)
# ret_dict = UXml.my_extract(_root, max_depth=10)
# time_consume = time.time() - _s
# print(f"perf:{int(1/time_consume)} tps")
# print(f"time_consume: {time_consume}")
# for k, v in ret_dict.items():
#     print(k, v)

a = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- scan:message测试 -->\n<books scan=\"message\">\n<book id=\"1001\">\n<name>API审计</name>\n<info>test</info>\n</book>\n<encoder>\n<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>\n</encoder><!-- scan:message测试 -->"
import random
random_num = random.randint(0, len(a))
c = a[:random_num]
print(c)
b = UXml.auto_fix_loads(c)
print(b)
"""